/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */

/*
 Copyright (C) 2011 Chris Kenyon


 This file is part of QuantLib, a free-software/open-source library
 for financial quantitative analysts and developers - http://quantlib.org/

 QuantLib is free software: you can redistribute it and/or modify it
 under the terms of the QuantLib license.  You should have received a
 copy of the license along with this program; if not, please email
 <quantlib-dev@lists.sf.net>. The license is also available online at
 <http://quantlib.org/license.shtml>.

 This program is distributed in the hope that it will be useful, but WITHOUT
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 FOR A PARTICULAR PURPOSE.  See the license for more details.
*/


#include <ql/types.hpp>
#include <ql/indexes/inflation/ukrpi.hpp>
#include <ql/termstructures/bootstraphelper.hpp>
#include <ql/time/calendars/unitedkingdom.hpp>
#include <ql/time/daycounters/actualactual.hpp>
#include <ql/time/daycounters/actual365fixed.hpp>
#include <ql/termstructures/yield/zerocurve.hpp>
#include <ql/indexes/ibor/gbplibor.hpp>
#include <ql/termstructures/inflation/inflationhelpers.hpp>
#include <ql/termstructures/inflation/piecewisezeroinflationcurve.hpp>
#include <ql/cashflows/indexedcashflow.hpp>
#include <ql/pricingengines/swap/discountingswapengine.hpp>
#include <ql/instruments/zerocouponinflationswap.hpp>
#include <ql/pricingengines/bond/discountingbondengine.hpp>
#include <ql/math/interpolations/bilinearinterpolation.hpp>

#include "utilities.hpp"

#include <ql/cashflows/cpicoupon.hpp>
#include <ql/cashflows/cpicouponpricer.hpp>
#include <ql/instruments/cpiswap.hpp>
#include <ql/instruments/bonds/cpibond.hpp>
#include <ql/instruments/cpicapfloor.hpp>

#include <ql/experimental/inflation/cpicapfloortermpricesurface.hpp>
#include <ql/experimental/inflation/cpicapfloorengines.hpp>


using namespace QuantLib;

using namespace std;

#include <iostream>


namespace {
    struct Datum {
        Date date;
        Rate rate;
    };

    template<class T, class U, class I>
    std::vector<std::shared_ptr<BootstrapHelper<T> > > makeHelpers(
            Datum iiData[], Size N,
            const std::shared_ptr<I> &ii, const Period &observationLag,
            const Calendar &calendar,
            const BusinessDayConvention &bdc,
            const DayCounter &dc) {

        std::vector<std::shared_ptr<BootstrapHelper<T> > > instruments;
        for (Size i = 0; i < N; i++) {
            Date maturity = iiData[i].date;
            Handle<Quote> quote(std::make_shared<SimpleQuote>(iiData[i].rate / 100.0));
            std::shared_ptr < BootstrapHelper<T> > anInstrument = std::make_shared<U>(
                    quote, observationLag, maturity,
                    calendar, bdc, dc, ii);
            instruments.emplace_back(anInstrument);
        }

        return instruments;
    }


    struct CommonVars {
        // common data

        Size length;
        Date startDate;
        Rate baseZeroRate;
        Real volatility;

        Frequency frequency;
        std::vector<Real> nominals;
        Calendar calendar;
        BusinessDayConvention convention;
        Natural fixingDays;
        Date evaluationDate;
        Natural settlementDays;
        Date settlement;
        Period observationLag, contractObservationLag;
        CPI::InterpolationType contractObservationInterpolation;
        DayCounter dcZCIIS, dcNominal;
        std::vector<Date> zciisD;
        std::vector<Rate> zciisR;
        std::shared_ptr<UKRPI> ii;
        RelinkableHandle<ZeroInflationIndex> hii;
        Size zciisDataLength;

        RelinkableHandle<YieldTermStructure> nominalUK;
        RelinkableHandle<ZeroInflationTermStructure> cpiUK;
        RelinkableHandle<ZeroInflationTermStructure> hcpi;

        vector<Rate> cStrikesUK;
        vector<Rate> fStrikesUK;
        vector<Period> cfMaturitiesUK;
        std::shared_ptr<Matrix> cPriceUK;
        std::shared_ptr<Matrix> fPriceUK;

        std::shared_ptr<CPICapFloorTermPriceSurface> cpiCFsurfUK;

        // cleanup

        SavedSettings backup;

        // setup
        CommonVars()
                : nominals(1, 1000000) {
            //std::cout <<"CommonVars" << std::endl;
            // option variables
            frequency = Annual;
            // usual setup
            volatility = 0.01;
            length = 7;
            calendar = UnitedKingdom();
            convention = ModifiedFollowing;
            Date today(1, June, 2010);
            evaluationDate = calendar.adjust(today);
            Settings::instance().evaluationDate() = evaluationDate;
            settlementDays = 0;
            fixingDays = 0;
            settlement = calendar.advance(today, settlementDays, Days);
            startDate = settlement;
            dcZCIIS = ActualActual();
            dcNominal = ActualActual();

            // uk rpi index
            //      fixing data
            Date from(1, July, 2007);
            Date to(1, June, 2010);
            Schedule rpiSchedule = MakeSchedule().from(from).to(to)
                    .withTenor(1 * Months)
                    .withCalendar(UnitedKingdom())
                    .withConvention(ModifiedFollowing);
            Real fixData[] = {
                    206.1, 207.3, 208.0, 208.9, 209.7, 210.9,
                    209.8, 211.4, 212.1, 214.0, 215.1, 216.8,   //  2008
                    216.5, 217.2, 218.4, 217.7, 216.0, 212.9,
                    210.1, 211.4, 211.3, 211.5, 212.8, 213.4,   //  2009
                    213.4, 214.4, 215.3, 216.0, 216.6, 218.0,
                    217.9, 219.2, 220.7, 222.8, -999, -999,     //  2010
                    -999};

            // link from cpi index to cpi TS
            bool interp = false;// this MUST be false because the observation lag is only 2 months
            // for ZCIIS; but not for contract if the contract uses a bigger lag.
            ii = std::make_shared<UKRPI>(interp, hcpi);
            for (Size i = 0; i < rpiSchedule.size(); i++) {
                ii->addFixing(rpiSchedule[i], fixData[i], true);// force overwrite in case multiple use
            };


            Datum nominalData[] = {
                    {Date(2, June, 2010),       0.499997},
                    {Date(3, June, 2010),       0.524992},
                    {Date(8, June, 2010),       0.524974},
                    {Date(15, June, 2010),      0.549942},
                    {Date(22, June, 2010),      0.549913},
                    {Date(1, July, 2010),       0.574864},
                    {Date(2, August, 2010),     0.624668},
                    {Date(1, September, 2010),  0.724338},
                    {Date(16, September, 2010), 0.769461},
                    {Date(1, December, 2010),   0.997501},
                    //{ Date( 16, December, 2010), 0.838164 },
                    {Date(17, March, 2011),     0.916996},
                    {Date(16, June, 2011),      0.984339},
                    {Date(22, September, 2011), 1.06085},
                    {Date(22, December, 2011),  1.141788},
                    {Date(1, June, 2012),       1.504426},
                    {Date(3, June, 2013),       1.92064},
                    {Date(2, June, 2014),       2.290824},
                    {Date(1, June, 2015),       2.614394},
                    {Date(1, June, 2016),       2.887445},
                    {Date(1, June, 2017),       3.122128},
                    {Date(1, June, 2018),       3.322511},
                    {Date(3, June, 2019),       3.483997},
                    {Date(1, June, 2020),       3.616896},
                    {Date(1, June, 2022),       3.8281},
                    {Date(2, June, 2025),       4.0341},
                    {Date(3, June, 2030),       4.070854},
                    {Date(1, June, 2035),       4.023202},
                    {Date(1, June, 2040),       3.954748},
                    {Date(1, June, 2050),       3.870953},
                    {Date(1, June, 2060),       3.85298},
                    {Date(2, June, 2070),       3.757542},
                    {Date(3, June, 2080),       3.651379}
            };
            const Size nominalDataLength = 33 - 1;

            std::vector<Date> nomD;
            std::vector<Rate> nomR;
            for (Size i = 0; i < nominalDataLength; i++) {
                nomD.emplace_back(nominalData[i].date);
                nomR.emplace_back(nominalData[i].rate / 100.0);
            }
            std::shared_ptr < YieldTermStructure > nominalTS
                    = std::make_shared<InterpolatedZeroCurve<Linear>>(nomD, nomR, dcNominal);


            nominalUK.linkTo(nominalTS);


            // now build the zero inflation curve
            observationLag = Period(2, Months);
            contractObservationLag = Period(3, Months);
            contractObservationInterpolation = CPI::Flat;

            Datum zciisData[] = {
                    {Date(1, June, 2011), 3.087},
                    {Date(1, June, 2012), 3.12},
                    {Date(1, June, 2013), 3.059},
                    {Date(1, June, 2014), 3.11},
                    {Date(1, June, 2015), 3.15},
                    {Date(1, June, 2016), 3.207},
                    {Date(1, June, 2017), 3.253},
                    {Date(1, June, 2018), 3.288},
                    {Date(1, June, 2019), 3.314},
                    {Date(1, June, 2020), 3.401},
                    {Date(1, June, 2022), 3.458},
                    {Date(1, June, 2025), 3.52},
                    {Date(1, June, 2030), 3.655},
                    {Date(1, June, 2035), 3.668},
                    {Date(1, June, 2040), 3.695},
                    {Date(1, June, 2050), 3.634},
                    {Date(1, June, 2060), 3.629},
            };
            zciisDataLength = 17;
            for (Size i = 0; i < zciisDataLength; i++) {
                zciisD.emplace_back(zciisData[i].date);
                zciisR.emplace_back(zciisData[i].rate);
            }

            // now build the helpers ...
            std::vector<std::shared_ptr<BootstrapHelper<ZeroInflationTermStructure> > > helpers =
                    makeHelpers<ZeroInflationTermStructure, ZeroCouponInflationSwapHelper,
                            ZeroInflationIndex>(zciisData, zciisDataLength, ii,
                                                observationLag,
                                                calendar, convention, dcZCIIS);

            // we can use historical or first ZCIIS for this
            // we know historical is WAY off market-implied, so use market implied flat.
            baseZeroRate = zciisData[0].rate / 100.0;
            std::shared_ptr < PiecewiseZeroInflationCurve<Linear> > pCPIts =
                    std::make_shared<PiecewiseZeroInflationCurve<Linear>>(
                            evaluationDate, calendar, dcZCIIS, observationLag,
                            ii->frequency(), ii->interpolated(), baseZeroRate,
                            Handle<YieldTermStructure>(nominalTS), helpers);
            pCPIts->recalculate();
            cpiUK.linkTo(pCPIts);
            hii.linkTo(ii);

            // make sure that the index has the latest zero inflation term structure
            hcpi.linkTo(pCPIts);

            // cpi CF price surf data
            Period cfMat[] = {3 * Years, 5 * Years, 7 * Years, 10 * Years, 15 * Years, 20 * Years, 30 * Years};
            Real cStrike[] = {3, 4, 5, 6};
            Real fStrike[] = {-1, 0, 1, 2};
            Size ncStrikes = 4, nfStrikes = 4, ncfMaturities = 7;

            Real cPrice[7][4] = {
                    {227.6,   100.27, 38.8,   14.94},
                    {345.32,  127.9,  40.59,  14.11},
                    {477.95,  170.19, 50.62,  16.88},
                    {757.81,  303.95, 107.62, 43.61},
                    {1140.73, 481.89, 168.4,  63.65},
                    {1537.6,  607.72, 172.27, 54.87},
                    {2211.67, 839.24, 184.75, 45.03}};
            Real fPrice[7][4] = {
                    {15.62, 28.38, 53.61,  104.6},
                    {21.45, 36.73, 66.66,  129.6},
                    {24.45, 42.08, 77.04,  152.24},
                    {39.25, 63.52, 109.2,  203.44},
                    {36.82, 63.62, 116.97, 232.73},
                    {39.7,  67.47, 121.79, 238.56},
                    {41.48, 73.9,  139.75, 286.75}};

            // now load the data into vector and Matrix classes
            cStrikesUK.clear();
            fStrikesUK.clear();
            cfMaturitiesUK.clear();
            for (Size i = 0; i < ncStrikes; i++) cStrikesUK.emplace_back(cStrike[i]);
            for (Size i = 0; i < nfStrikes; i++) fStrikesUK.emplace_back(fStrike[i]);
            for (Size i = 0; i < ncfMaturities; i++) cfMaturitiesUK.emplace_back(cfMat[i]);
            cPriceUK = std::make_shared<Matrix>(ncStrikes, ncfMaturities);
            fPriceUK = std::make_shared<Matrix>(nfStrikes, ncfMaturities);
            for (Size i = 0; i < ncStrikes; i++) {
                for (Size j = 0; j < ncfMaturities; j++) {
                    (*cPriceUK)[i][j] = cPrice[j][i] / 10000.0;
                }
            }
            for (Size i = 0; i < nfStrikes; i++) {
                for (Size j = 0; j < ncfMaturities; j++) {
                    (*fPriceUK)[i][j] = fPrice[j][i] / 10000.0;
                }
            }
        }
    };

}


TEST_CASE("InflationCPICapFloor_cpicapfloorpricesurface", "[InflationCPICapFloor]") {

    // check inflation leg vs calculation directly from inflation TS
    CommonVars common;


    Real nominal = 1.0;
    InterpolatedCPICapFloorTermPriceSurface
            <Bilinear> cpiSurf(nominal,
                               common.baseZeroRate,
                               common.observationLag,
                               common.calendar,
                               common.convention,
                               common.dcZCIIS,
                               common.hii,
                               common.nominalUK,
                               common.cStrikesUK,
                               common.fStrikesUK,
                               common.cfMaturitiesUK,
                               *(common.cPriceUK),
                               *(common.fPriceUK));

    // test code - note order of indices
    for (Size i = 0; i < common.fStrikesUK.size(); i++) {

        Real qK = common.fStrikesUK[i];
        Size nMat = common.cfMaturitiesUK.size();
        for (Size j = 0; j < nMat; j++) {
            Period t = common.cfMaturitiesUK[j];
            Real a = (*(common.fPriceUK))[i][j];
            Real b = cpiSurf.floorPrice(t, qK);

            QL_REQUIRE(fabs(a - b) < 1e-7, "cannot reproduce cpi floor data from surface: "
                    << a << " vs constructed = " << b);
        }

    }

    for (Size i = 0; i < common.cStrikesUK.size(); i++) {

        Real qK = common.cStrikesUK[i];
        Size nMat = common.cfMaturitiesUK.size();
        for (Size j = 0; j < nMat; j++) {
            Period t = common.cfMaturitiesUK[j];
            Real a = (*(common.cPriceUK))[i][j];
            Real b = cpiSurf.capPrice(t, qK);

            QL_REQUIRE(fabs(a - b) < 1e-7, "cannot reproduce cpi cap data from surface: "
                    << a << " vs constructed = " << b);
        }
    }

    // remove circular refernce
    common.hcpi.linkTo(std::shared_ptr < ZeroInflationTermStructure > ());
}


TEST_CASE("InflationCPICapFloor_cpicapfloorpricer", "[InflationCPICapFloor]") {

    CommonVars common;
    Real nominal = 1.0;
    std::shared_ptr < CPICapFloorTermPriceSurface >
    cpiCFpriceSurf = std::make_shared<InterpolatedCPICapFloorTermPriceSurface<Bilinear>>(nominal, common.baseZeroRate,
                                                                                         common.observationLag,
                                                                                         common.calendar,
                                                                                         common.convention,
                                                                                         common.dcZCIIS, common.hii,
                                                                                         common.nominalUK,
                                                                                         common.cStrikesUK,
                                                                                         common.fStrikesUK,
                                                                                         common.cfMaturitiesUK,
                                                                                         *(common.cPriceUK),
                                                                                         *(common.fPriceUK));
    common.cpiCFsurfUK = cpiCFpriceSurf;

    // interpolation pricer first
    // N.B. no new instrument required but we do need a new pricer

    Date startDate = Settings::instance().evaluationDate();
    Date maturity(startDate + Period(3, Years));
    Calendar fixCalendar = UnitedKingdom(), payCalendar = UnitedKingdom();
    BusinessDayConvention fixConvention(Unadjusted), payConvention(ModifiedFollowing);
    Rate strike(0.03);
    Real baseCPI = common.hii->fixing(fixCalendar.adjust(startDate - common.observationLag, fixConvention));
    CPI::InterpolationType observationInterpolation = CPI::AsIndex;
    CPICapFloor aCap(Option::Call,
                     nominal,
                     startDate,   // start date of contract (only)
                     baseCPI,
                     maturity,    // this is pre-adjustment!
                     fixCalendar,
                     fixConvention,
                     payCalendar,
                     payConvention,
                     strike,
                     common.hii,
                     common.observationLag,
                     observationInterpolation);

    Handle<CPICapFloorTermPriceSurface> cpiCFsurfUKh(common.cpiCFsurfUK);
    std::shared_ptr < PricingEngine > engine = std::make_shared<InterpolatingCPICapFloorEngine>(cpiCFsurfUKh);

    aCap.setPricingEngine(engine);

    Date d = common.cpiCFsurfUK->cpiOptionDateFromTenor(Period(3, Years));


    Real cached = cpiCFsurfUKh->capPrice(d, strike);
    QL_REQUIRE(fabs(cached - aCap.NPV()) < 1e-10, "InterpolatingCPICapFloorEngine does not reproduce cached price: "
            << cached << " vs " << aCap.NPV());

    // remove circular refernce
    common.hcpi.linkTo(std::shared_ptr < ZeroInflationTermStructure > ());
}

